Integracja Facebook @workplace z SharePoint, z użyciem Microsoft Flow
Table of Contents
Gdy budowałem intranet używając modern SharePoint zostałem zapytany, czy byłoby możliwe, by wyświetlać w nim informacje pochodzące z ich portalu facebook @workplace. Zacząłem szukać i przeglądać dokumentację po to tylko, by dowiedzieć się, że bez znaczenia będzie fakt, czy użyję podejścia modern, czy classic w SharePoint, @workplace i tak nie posiada gotowych skryptów, które mógłbym skopiować i wkleić do portalu, by móc np. wyświetlać dane z Newsfeed.
Rozwiązanie
Zależało mi na znalezieniu takiego rozwiązania, które będzie wykorzystywać dostępne możliwości pudełkowe, nie zaś na zaprogramowaniu dedykowanego rozwiązania. Po przeczytaniu dokumentacji API @workplace na temat „custom integrations” (źródło) zdecydowałem się na stworzenie rozwiązania, które będzie wykorzystywać poniższe aplikacje i funkcjonalności:
- @workplace używa webhooków do wzbudzania Microsoft Flow (źródło 1: webhooks getting started, źródło 2: webhooks in @workplace)
- Microsoft Flow otrzymuje żądanie, przetwarza je i zapisuje dane na liście SharePoint
- Dedykowany web part w SharePoint pokazuje dane z listy
Wymagania wstępne
Custom integration w @workplace
By w ogóle móc użyć webhooków, najpierw należy zdefiniować customową intgrację. Do wykonania tego musisz przejść na stronę „Integrations” (będąc administratorem @workplace): https://[your-company].facebook.com/work/admin/?section=apps&ref=bookmarks i utworzyć nową integrację:
Następnie zdefiniuj jej nazwę i opcjonalnie opis.
Teraz musisz wybrać uprawnienia, jakie ma posiadać (w moim przypadku było to wyłącznie „Read group content”), następnie wskazać grupy, z jakich dane mają być pobierane (w moim przypadku „All groups”) i na koniec skonfigurować webhooki.
[tds_info] Pamiętaj, że jeden adres URL może być subskrybowany do wyłącznie jednego topica w webhooku, jednak wiele topiców, w jednym webhooku, może używać tego samego adresu URL.[/tds_info]
W moim przypadku skonfigurowałem wyłącznie webhooka dla „Groups”. Jak? Czytaj dalej.
Weryfikacja webhook
By móc zapisać dane webhooka należy go zweryfikować poprzez wysłanie żądania pod callback URL. W naszym przypadku callback URL to po prostu adres URL Microsoft Flow, którego będziemy używać do odbierania i przetwarzania danych.
[tds_info] Warto zwrócić uwagę, że weryfikacja adresu callback musi zostać wykonana poprzez odebranie żądania GET, tymczasem wszystkie dalsze żądania z webhooka będą realizowane przy użyciu akcji POST.[/tds_info]
Sam Flow jest dość prosty. Najpierw akcja „Request”, która uruchamia przepływ. Następnie parsowanie danych z query stringa, używając akcji „Parse JSON”:
- Content: triggerOutputs()[’queries’]
- Schema:
{ "type": "object", "properties": { "hub.mode": { "type": "string" }, "hub.challenge": { "type": "string" }, "hub.verify_token": { "type": "string" } } }
I na koniec akcja „Response”, która używa wartości parametru „hub.challenge” jako Body:
Nie zapomnij zmienić sposobu, w jaki Flow ma być wyzwalany. Ustaw „GET” (1) w polu „method” (znajduje się w sekcji „advanced options”), opublikuj workflow i skopiuj jego adres URL (2):
Teraz wklej ten adres w polu „Callback URL”, w konfiguracji webhooka i wpisz dowolną wartość w pole „Verify Token” (ta wartość powinna być użyta do weryfikacji, czy żądanie odebrane z GET jest faktycznie wysłane z Twojego webhooka) i zapisz:
Gotowe! Zauważysz, na liście historii uruchomień Twojego Flow, że zakończył się z sukcesem, zaś nowo przygotowana „Custom integration” zostanie zapisana.
[tds_info] Kiedy zdecydujesz o konieczności zmiany CZEGOKOLWIEK w konfiguracji swojej Custom Integration, będziesz musiał wykonać weryfikację Callback URL ponownie – czyli ponownie odebrać żądanie używając GET.[/tds_info]
Budowa Flow
Pamiętaj, by zachować akcje do obsługi żądań GET w Twoim Flow. Nie usuwaj ich. Ja to zrobił poprzez utworzenie akcji warunkowej z ustawionym warunkiem „1 jest równe 2”, także za każdym razem Flow wykona ścieżkę „No”. W razie konieczności weryfikacji „Custom Integration” po prostu ręcznie zmieniam rodzaj wywołania mojego Flow z „POST” na „GET” i warunek na „1 równa się 1”. Po weryfikacji z powrotem ustawiam wywołanie na „POST”, a warunek na „1 jest równe 2”.
Schemat request body
W gałęzi obsługującej żądania „POST’, pierwsza akcja to „Parse JSON”, której używam do przetworzenia treści żądania. Zanim stworzyłem poprawny schemat , wykonałem dziesiątki testów pisząc różne posty i komentarze w @workplace i podglądając strukturę JSON tych żądań. Na ich podstawie przygotowałem jeden, spójny plik, który obsługuje poniższe scenariusze:
- Membership – gdy użytkownik dołącza do grupy
- Comment – gdy dodany jest nowy komentarz
- Post – gdy dodany jest nowy post
- Post z pojedynczym zdjęciem
- Post z wieloma zdjęciami
- Post bez zdjęcia (status)
- Wydarzenie
- inne
Finalnie schemat wygląda jak poniżej (zawiera atrybuty dla wszystkich scenariuszy, jednak oznaczone jako niewymagane – brak atrybutu „required”):
{ "type": "object", "properties": { "entry": { "type": "array", "items": { "type": "object", "properties": { "changes": { "type": "array", "items": { "type": "object", "properties": { "field": { "type": "string" }, "value": { "type": "object", "properties": { "from": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } }, "member": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } }, "update_time": { "type": "string" }, "verb": { "type": "string" }, "community": { "type": "object", "properties": { "id": { "type": "string" } } }, "actor": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } }, "attachments": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "url": { "type": "string" }, "subattachments": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "url": { "type": "string" }, "media": { "type": "object", "properties": { "image": { "type": "object", "properties": { "src": { "type": "string" }, "width": { "type": "number" }, "height": { "type": "number" } } } } }, "type": { "type": "string" }, "target": { "type": "object", "properties": { "url": { "type": "string" }, "id": { "type": "string" } } }, "title": { "type": "string" } } } } } }, "media": { "type": "object", "properties": { "image": { "type": "object", "properties": { "src": { "type": "string" }, "width": { "type": "number" }, "height": { "type": "number" } } } } }, "type": { "type": "string" }, "description": { "type": "string" }, "target": { "type": "object", "properties": { "url": { "type": "string" }, "id": { "type": "string" } } }, "title": { "type": "string" } } } } } }, "type": { "type": "string" }, "target_type": { "type": "string" }, "comment_id": { "type": "string" }, "post_id": { "type": "string" }, "created_time": { "type": "string" }, "message": { "type": "string" }, "permalink_url": { "type": "string" } } } } } }, "id": { "type": "string" }, "time": { "type": "number" } }, "required": [ "changes", "id", "time" ] } }, "object": { "type": "string" } } }
Flow krok po kroku
Po przetworzeniu żądania, Flow realizuje poniższe kroki:
- Pobierz typ operacji – odczytanie, czy jest to „Comment”, „Membership”, czy „Posts”
Robię to używając akcji „Compose” i poniższego wyrażenia:body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['field']
- Akcja „Switch” w zależności od typu operacji.
- Dla każdego rodzaju typu najpierw odczytuję właściwe mu ID (np. comment_id dla „Comments” czy post_id dla „Posts”)
- Następnie używając akcji Query SharePoint sprawdzam, czy istnieje już rekord dla podanego ID. Jeśli tak, workflow się kończy.
- Jeśli scenariusz dotyczy rodzaju „Post”, wtedy Flow szuka wewnętrznego rodzaju operacji używając poniższego wyrażenia:
body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['field']
- W zależności od wyniku, Flow używając akcji „Switch” wykonuje realizuje odpowiedni scenariusz:
- Photo
- Status
- Event
- i inne rodzaje (choć jak dotąd, nic innego od powyższych nie widziałem)
- Następnie, jeśli jest to typ „Photo” Flow sprawdza, czy w żądaniu zawarte jest tylko jedno zdjęcie, czy wiele (galeria). Sprawdza to, używając poniższego wyrażenia, badając czy typ to „album”:
body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['value']['attachments']['data']?[0]?['type']
- Na koniec próbuje zmapować autora informacji z istniejącym użytkownikiem SharePoint. W tym celu używa akcji „Office 365 Get user profile (V2)”, podając jako UPN złączony ciąg tekstów: imię, nazwisko i domena organizacji:
concat(trim(replace(trim(body('Parse_request_body')?['entry']?[0]?['changes']?[0]?['value']['from']['name']), ' ', '.')), '@the-company.com')
- Oczywiście, nie zawsze użytkownik musi istnieć. W takim wypadku zwykle akcja zwraca błąd, jednak z punktu widzenia logiki, nie jest to błąd, który powinien zatrzymać przepływ. By temu zapobiec, skonfigurowałem ustawienia „run after” w akcji tworzącej rekord w SharePoint, by uruchamiała się także w sytuacji, gdy akcja odczytywania profilu użytkownika zakończy się błędem:
- Na koniec Flow tworzy nowy element na liście, łącząc wszystkie informacje w całość:
Struktura akcji Flow, na średnim poziomie szczegółowości wygląda dla opisywanego rozwiązania jak poniżej:
Widoczne obok strzałek ikonki (i) oznaczają relacje, w których kolejne akcje wykonywane są nawet w przypadku, gdy poprzednia zakończy się niepowodzeniem.
Budowa pojedynczego bloku, zapisującego dane do list SharePoint, wygląda jak na poniższym przykładzie (dla zapisu danych nowego posta):
Struktura danych
Dla przechowywania komentarzy używam jednej listy w SharePoint zbudowanej z poniższych kolumn:
- Title – przechowuje informację o typie wiadomości.
- Author – kolumna tekstowa, do przechowywania imienia i nazwiska autora wiadomości.
- Date – data wystąpienia zdarzenia (pobrana z webhooka).
- Post/ Comment – wielowierszowe pole tekstowe, pozwalające na formatowanie HTML, służące do przechowywania treści wiadomości.
- Image – ponownie jest to wielowierszowe pole tekstowe, ponownie zezwalające na formatowanie HTML, służące do przechowywania tagu <img> z adresem URL obrazu, dołączonego do wiadomości. Jest to spowodowane faktem, że Flow nie wspiera aktualnie zapisu do pól „Picture/ Hyperlink” z ustawionym typem „Picture”.
- SourceURL – pole „Picture/ Hyperlink” z ustawionym typem „Hyperlink”.
- AuthorPPL – drugie pole dla przechowywania danych autora, tym razem jest to typ „Person or group”. Workflow próbuje zmapować dane tekstowe autora z istniejącym kontem użytkownika w SharePoint.
- ItemId – identyfikator wiadomości, w zależności od typu: comment_id, post_id lub member_id.
Lista zasilona danymi wygląda jak poniżej:
Tematy do zapamiętania
Podczas tego projektu nauczyłem się poniższych rzeczy. Sugeruję o nich pamiętać, gdy będziesz podążać krokami mojego rozwiązania:
- @workplace będzie wysyłać żądania GET do FLOW za każdym razem, gdy jakakolwiek zmiana w „Custom integration” zostaje dokonana i zapisana.
- @workplace będzie wysyłać pojedyncze żądania POST tak długo, aż nie dostanie zwrotnie odpowiedzi „200 OK”. Należy pamiętać o tym, by weryfikować czy przetwarzana wiadomość już istnieje na liście i nie dodawać jej po raz kolejny.
- @workplace nie zawsze czeka na odpowiedź. W moim przepływie widzę dziesiątki nieudanych uruchomień tylko dlatego, że @workplace nie czeka na odpowiedź. Nie wiem dlaczego i zachowanie to jest całkowicie przypadkowe (czasem przepływy trwające dłużej kończą się poprawnie, a krótsze błędem). W każdym razie – gdy @workplace nie otrzyma odpowiedzi, będzie ponawiać wysyłanie wiadomości tak długo, aż dostanie „200 OK”, z tymże będzie robić to coraz rzadziej. Po pewnym czasie zaprzestanie wysyłania danej wiadomości.
- Używanie „run after” w konfiguracji akcji w Flow jest naprawdę użyteczne – zarówno do zapobiegania błędom w wykonywaniu akcji w przypadku braku wymaganych danych, czy braku użytkownika – takich, które w logice przepływu nie powinny powodować jego zakończenia, czy np. w celu tworzenia warunków w logice. W moim Flow używałem ich naprawdę często.
Dzięki za dotarcie aż do tego zdania! Mam nadzieję, że ten opis Ci pomoże. W razie czego, nie krępuj się i pytaj lub pozostaw komentarz poniżej.